package fr.cephb.locustree;

import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;


import java.util.logging.Level;
import java.util.logging.Logger;
import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.bind.tuple.LongBinding;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;


import fr.cephb.berkeleydb.binding.IStringBinding;
import fr.cephb.berkeleydb.db.DuplicateDB;
import fr.cephb.berkeleydb.db.PrimaryDB;
import fr.cephb.berkeleydb.db.Walker;
import fr.cephb.berkeleydb.sort.LongComparator;


/**
 * 
 * LTModel
 *
 */
public class LTModel
	{
	static final Logger LOG= Logger.getLogger("fr.cephb");
	private Environment environment=null;
	private PrimaryDB<Integer,Organism> organismDB=null;
	private PrimaryDB<Integer,ChromInfo> chromosomesDB=null;
	private PrimaryDB<Long, LTNode> id2locusNodes=null;
	private PrimaryDB<TrackChrom, Long> trackChrom2NodeId=null;
	private PrimaryDB<Integer,Track> tracksDB=null;
	private DuplicateDB<String, Long> keyword2nodeId=null;
	
	private Boolean forceTransactional=null;
	
	/**
	 * LTModel
	 */
	public LTModel()
		{
		
		}
	
	
	
	public Environment getEnvironment()
		{
		return environment;
		}
	
	protected EnvironmentConfig createEnvironmentConfig()
		{
		EnvironmentConfig envConfig= new EnvironmentConfig();
		return envConfig;
		}
	
	/**
	 * Open
	 * @param dbHome
	 * @param write
	 * @throws DatabaseException
	 */
	public void open(File dbHome, boolean write) throws DatabaseException
		{
		LOG.info("opening "+dbHome+" write:"+write);
		boolean transactional=write;
		if(forceTransactional!=null && write)
			{
			transactional=forceTransactional;
			LOG.info("environement was forced to be transactional="+transactional);
			}
		
		Transaction txn=null;
		EnvironmentConfig envConfig= createEnvironmentConfig();
		envConfig.setConfigParam(EnvironmentConfig.LOG_FILE_MAX,"200000000"/* 200 Mo */);
		//envConfig.setCacheSize(100 * 1024 * 1024 ); 
		envConfig.setAllowCreate(write);
		envConfig.setReadOnly(!write);
		envConfig.setTransactional(transactional);
		
		this.environment= new Environment(dbHome, envConfig);
		
		
		
		DatabaseConfig cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		this.organismDB=new PrimaryDB<Integer, Organism>(this.environment, txn, "organisms", cfg,
					new IntegerBinding(),new Organism.Binding()
					);
		cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		this.chromosomesDB=new PrimaryDB<Integer, ChromInfo>(
				this.environment, txn, "chromosomes", cfg,
				new IntegerBinding(),new ChromInfo.Binding()
				);
		cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		cfg.setBtreeComparator(LongComparator.class);
		this.id2locusNodes=new PrimaryDB<Long, LTNode>(
				this.environment, txn, "ltnodes", cfg,
				new LongBinding(),new LTNode.Binding()
				);
		
		cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		this.trackChrom2NodeId=new PrimaryDB<TrackChrom, Long>(
				this.environment, txn, "trackchrom2nodeid", cfg,
				new TrackChrom.Binding(),new LongBinding()
				);
		
		cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		this.tracksDB=new PrimaryDB<Integer, Track>(
				this.environment, txn, "tracks", cfg,
				new IntegerBinding(),new Track.Binding()
				);
		
		cfg= new DatabaseConfig();
		cfg.setAllowCreate(write);
		cfg.setReadOnly(!write);
		cfg.setTransactional(transactional);
		cfg.setSortedDuplicates(true);
		this.keyword2nodeId= new DuplicateDB<String, Long>(
				this.environment, txn, 
				"kwd2nodeid", cfg, 
				new IStringBinding(IStringBinding.TO_LOWERCASE), 
				new LongBinding()
				);
	
		LOG.info("end opening");
		}
	
	
	
	
	public void close() throws DatabaseException
		{
		if(this.keyword2nodeId!=null) this.keyword2nodeId.close();
		this.keyword2nodeId=null;
		if(this.tracksDB!=null) this.tracksDB.close();
		this.tracksDB=null;
		if(this.id2locusNodes!=null) this.id2locusNodes.close();
		this.id2locusNodes=null;
		if(this.trackChrom2NodeId!=null) this.trackChrom2NodeId.close();
		this.trackChrom2NodeId=null;
		if(this.chromosomesDB!=null) this.chromosomesDB.close();
		this.chromosomesDB=null;
		if(this.organismDB!=null) this.organismDB.close();
		this.organismDB=null;
		if(this.environment!=null) 
			{
			if(!this.environment.getConfig().getReadOnly())
				{
				this.environment.cleanLog();
				}
			this.environment.close();
			this.environment=null;
			}
		
		}
	
	
	public void setTransactional(Boolean forceTransactional)
		{
		this.forceTransactional = forceTransactional;
		}
	
	public PrimaryDB<TrackChrom, Long> getTrackChrom2NodeId()
		{
		return trackChrom2NodeId;
		}
	
	public PrimaryDB<Integer, Track> getTracksDB()
		{
		return tracksDB;
		}
	
	public PrimaryDB<Integer, ChromInfo> getChromDB()
		{
		return chromosomesDB;
		}
	
	public PrimaryDB<Integer, Organism> getOrganismDB()
		{
		return organismDB;
		}
	
	public PrimaryDB<Long, LTNode> getLocusNodeDB()
		{
		return id2locusNodes;
		}
	
	public DuplicateDB<String, Long> getKeyword2nodeId() {
		return keyword2nodeId;
		}
	
	public Organism getOrganismByName(Transaction txn,String orgName)
		throws DatabaseException
		{
		Walker<Integer, Organism> w=null;
		try
			{
			w= getOrganismDB().openWalker(txn);
			while(w.next())
				{
				//System.err.println(""+w.getValue().getOrganismId()+" vs "+organismId);
				if(w.getValue().getName().equals(orgName))
					{
					return w.getValue();
					}
				}
			return null;
			}
		catch (DatabaseException e)
			{
			throw e;
			}
		finally
			{
			if(w!=null) w.close();
			}
		}
	
	public ChromInfo getChromsomeByName(
			Transaction txn,
			Integer organismId,
			String name
			) throws DatabaseException
			{
			Walker<Integer, ChromInfo> w=null;
			try
				{
				w= getChromDB().openWalker(txn);
				while(w.next())
					{
					ChromInfo ci=w.getValue();
					if(ci.getOrganismId().equals(organismId) &&
					   ci.getName().equals(name))
						{
						
						return ci;
						}
					}
				return null;
				}
			catch (DatabaseException e)
				{
				throw e;
				}
			finally
				{
				if(w!=null) w.close();
				}
			}
	
	
	public List<ChromInfo> getChromsomesByOrganismId(
		Transaction txn,
		Integer organismId
		) throws DatabaseException
		{
		//System.err.println("getChromsomesByOrganisms "+organismId);
		List<ChromInfo> list= new ArrayList<ChromInfo>(30);
		if(organismId==null) return list;
		Walker<Integer, ChromInfo> w=null;
		try
			{
			w= getChromDB().openWalker(txn);
			while(w.next())
				{
				//System.err.println(""+w.getValue().getOrganismId()+" vs "+organismId);
				if(w.getValue().getOrganismId().equals(organismId))
					{
					list.add(w.getValue());
					}
				}
			return list;
			}
		catch (DatabaseException e)
			{
			throw e;
			}
		finally
			{
			if(w!=null) w.close();
			}
		}
	
	public void scan(Transaction txn,LTScanner scanner)
	throws DatabaseException
		{
		Long id= this.trackChrom2NodeId.get(txn,new TrackChrom(scanner.getTrackId(),scanner.getChromosomeId()));
		if(id==null) return;
		LTNode node=findNodeById(txn,id);
		scan(txn,node,scanner);
		}
	
	private void scan(Transaction txn, final LTNode root,final LTScanner scanner) throws DatabaseException
		{
		if(scanner==null || scanner.isAbortCalled()) return;
		
		//LOG.info("0: root "+root.getStart()+" - "+root.getEnd() +" scanner:"+scanner.getStart());
		
		if((scanner.getStart()+scanner.getLength())< root.getStart())
			{
			//LOG.info("break 1 : "+(scanner.getStart()+scanner.getLength())+" < "+ root.getStart());
			return;
			}
		
		
		
		if(root.getEnd()<scanner.getStart())
			{
			//LOG.info("break 2");
			return;
			}
		
		if(root.hasEntities())
			{
			for(MappedObject mapped: root.getEntities() )
				{
				if(scanner.isAbortCalled()) return;
				scanner.entity(mapped);
				}
			}
		//System.err.println(""+ root.getLength()+"/"+ scanner.getResolution());
		if( root.getLength()< scanner.getResolution())
			{
			//LOG.info("1: "+root.getLength()+" "+scanner.getResolution());
			/* fill histogram */	
			if(root.getCountEntitiesBelow()>0)
				{
				scanner.histogram(root);
				}
			//LOG.info("return  3");
			return;
			}
		
		if(root.sub_nodes_ids!=null )//&& this.getChildResolution()>=scanner.resolution)
			{
			//LOG.info(":3");
			for(int i=0; i< root.sub_nodes_ids.size();++i)
				{
				if(scanner.isAbortCalled()) return;
				Long subid= root.sub_nodes_ids.get(i);
				if(subid==null || subid==LTNode.NIL) continue;
				LTNode child= findNodeById(txn,subid);
				assert(child!=null);
				scan(txn,child,scanner);
				}
			}
		}
	
	public LTNode findNodeById(Transaction txn,long id)
		{
		if(id==LTNode.NIL) return null;
		return getLocusNodeDB().get(txn, id);
		}

	/** answer the root node from a given node Id */
	public LTNode getRootFromNodeId(Transaction txn,Long node_id)
		throws DatabaseException
		{
		if(node_id==null) return null;
		LTNode curr= findNodeById(txn, node_id);
		if(curr==null) return null;
		
		while(curr.parent_id!=LTNode.NIL)
			{
			curr= findNodeById(txn, curr.parent_id);
			}
		return curr;
		}
	
	public TrackChrom getTrackChromFromNodeId(Transaction txn,Long id)
		throws DatabaseException
		{
		LTNode root= getRootFromNodeId(txn, id);
		if(root==null) return null;
		Walker<TrackChrom, Long> w=null;
		try
			{
			w=getTrackChrom2NodeId().openWalker(txn);
			while(w.next())
				{
				if(w.getValue().equals(root.getId())) return w.getKey();
				}
			return null;
			}
		catch (DatabaseException e)
			{
			throw e;
			}
		finally
			{
			if(w!=null) w.close();
			}
		}
	
	
	public Organism getOrganismFromChromosomeId(Transaction txn,Integer chrom_id)
	throws DatabaseException
		{
		if(chrom_id==null) return null;
		ChromInfo ci= getChromDB().get(txn, chrom_id);
		if(ci==null) return null;
		return getOrganismDB().get(txn,ci.getOrganismId());
		}
	
	private Long nextNodeId(Transaction txn) throws DatabaseException
		{
		Walker<Long, LTNode> w= this.getLocusNodeDB().openWalker(txn);
		if(w.getLast()==OperationStatus.SUCCESS)
			{
			w.close();
			long newkey=1L+w.getKey();
			return newkey;
			}
		w.close();
		return 1L;
		}
	
	private void save(Transaction txn,LTNode node)throws DatabaseException
		{
		assert(node!=null);
		assert(node.id!=LTNode.NIL);
		assert(node.id>0L);
		assert(node.id!=node.parent_id);
		assert(node.id>node.parent_id);
		getLocusNodeDB().put(txn, node.id, node);
		}

	private long add(
			Transaction txn,
			LTNode node,
			Track track,
			MappedObject objectmapped
			) throws DatabaseException
		{
		assert(node.id>0);
		assert(objectmapped.getStart()<=objectmapped.getEnd());
		boolean need_save_node=false;
		//boolean object_was_added=false;
		long node_id_added=LTNode.NIL;
		
		if(objectmapped.size()> node.max_length_object)
			{
			node.max_length_object=objectmapped.size();
			need_save_node=true;
			}
		
		if(  node.isRoot() ||
			  (
			  node.getLength()> objectmapped.size() &&
			  node.getLength()/LTNode.CHILDREN_PER_NODE> track.end_resolution
			  )
			)
			{
			
			if(node.isRoot())
				{
				node.length=Math.max(node.length,objectmapped.getEnd());
				}
			final int child_size = node.isRoot()?track.start_resolution: node.getLength()/LTNode.CHILDREN_PER_NODE;
			final int child_index=(objectmapped.getStart()-node.start)/child_size;
			assert(node.isRoot() || child_index< LTNode.CHILDREN_PER_NODE);
			Long subid=node.getSubNodeIdAt(child_index);
			LTNode sub=null;
			if(subid==null || subid==LTNode.NIL)
				{
				
				sub= new LTNode();
				sub.parent_id=node.id;
				sub.id= nextNodeId(txn);
				sub.start= node.start + child_index* child_size;
				sub.length= child_size;
				
				assert(node.isRoot() || sub.length< node.length);
				assert(sub.length>=1);
				sub.level= node.getLevel()+1;
				save(txn,sub);//commit 'sub.id'
				node.setSubNodeIdAt(child_index, sub.id);
				need_save_node=true;
				}
			else
				{
				sub= findNodeById(txn,subid);
				assert(sub!=null);
				}
			node_id_added =add(txn,sub,track,objectmapped);
			if(node_id_added!=LTNode.NIL)
				{
				node.count_entities_below++;
				need_save_node=true;
				}
			}
		else
			{
			/* bad assertion below: parent is 1000, current is 100 and object is 900 */
			//assert(objectmapped.size()<=node.getLength());
			if(node.entities==null) node.entities=new HashSet<MappedObject>();
			boolean object_was_added=node.entities.add(objectmapped);
			if(object_was_added)
				{
				node_id_added= node.id;
				need_save_node=true;
				}
			}
		if(need_save_node)
			{
			save(txn,node);
			}
		return node_id_added;
		}
	
	public void dump(Transaction txn,PrintStream out)
		throws DatabaseException
		{
		Walker<TrackChrom,Long> w=this.trackChrom2NodeId.openWalker(null);
		while(w.getNext()==OperationStatus.SUCCESS)
			{
			out.println("= TRACK ID. "+w.getKey().getTrackId()+" / CHROM: "+w.getKey().getChromosomeId());
			_dump(txn,out,"",findNodeById(null,w.getValue()));
			}
		w.close();
		}
	
	private void _dump(Transaction txn,PrintStream out,String prefix,LTNode node)
	throws DatabaseException
		{
		if(node==null) return;
		out.println(prefix+"id:"+node.getId());
		out.println(prefix+"level:"+node.getLevel());
		out.println(prefix+"start:"+node.start);
		out.println(prefix+"length:"+node.getLength());
		out.println(prefix+"count-below:"+node.count_entities_below);
		if(node.entities==null)
			{
			out.println(prefix+"entities:0");
			}
		else
			{
			out.print(prefix+"entities:");
			for(MappedObject o:node.entities)
				{
				out.print(" "+o.getId()+":"+o.size());
				}
			out.println();
			}
		if(node.sub_nodes_ids!=null)
			{
			for(Long L:node.sub_nodes_ids)
				{
				_dump(txn,out,prefix+"    ",findNodeById(null,L));
				}
			}
		}
	
	/**
	 * returns the NodeId
	 */
	public long insert(Transaction txn,
			final Integer chromosome_id,
			MappedObject mapped,
			Track track) throws DatabaseException
		{
		LTNode root= null;
		boolean save_chrom=false;
		ChromInfo ci= getChromDB().get(txn, chromosome_id);
		if(ci==null)
			{
			throw new IllegalArgumentException("chromosome id "+chromosome_id+" was not configured");
			}
		if(mapped.getEnd()> ci.getLength())
			{
			save_chrom=true;
			ci.setLength(mapped.getEnd());
			}
		if(save_chrom)
			{
			getChromDB().put(txn, chromosome_id,ci);
			}
		
		
		final TrackChrom trackChrom = new TrackChrom(track.getId(),chromosome_id);
		Long rootId= this.trackChrom2NodeId.get(txn, trackChrom);
		if(rootId==null)
			{
			root= new LTNode();
			root.id= nextNodeId(txn);
			//LOG.info("creating trackchrom for "+trackChrom+" node.id="+root.id);
			save(txn,root);//commit 'sub.id'
			this.trackChrom2NodeId.put(txn,trackChrom,root.id); 
			}
		else
			{
			root = findNodeById(txn,rootId);
			assert(root!=null);
			}
		long nodeId=add(txn,root, track, mapped);
		return nodeId;
		}

	
	

	public static void main(String[] args)
		{
		LOG.setLevel(Level.OFF);
		LTModel model=null;
		try
			{
			File dbHome=null;
			String program=null;
			int optind=0;
			while(optind< args.length)
				{
				if(args[optind].equals("-h") ||
				   args[optind].equals("-help") ||
				   args[optind].equals("--help"))
					{
					System.err.println("Options:");
					System.err.println(" -h help; This screen.");
					return;
					}
				else if(args[optind].equals("-db-home"))
					{
					dbHome=new File(args[++optind]);
					}
				else if(args[optind].equals("-log-level"))
					{
					LOG.setLevel(Level.parse(args[++optind]));
					}
				else if(args[optind].equals("-p"))
					{
					program= args[++optind];
					}
				else if(args[optind].equals("--"))
					{
					optind++;
					break;
					}
				else if(args[optind].startsWith("-"))
					{
					System.err.println("Unknown option "+args[optind]);
					return;
					}
				else 
					{
					break;
					}
				++optind;
				}
			if(dbHome==null)
				{
				System.err.println("-db-home undefined");
				return;
				}
			
			
			if(program==null)
				{
				System.err.println("Program undefined");
				}
			else if(program.equals("dump"))
				{
				model=new LTModel();
				model.open(dbHome,false);
				model.dump(null,System.out);
				}
			else
				{
				System.err.println("unknown program "+program);
				}
			
			} 
		catch(Throwable err)
			{
			err.printStackTrace();
			}
		finally
			{
			if(model!=null)
				{
				try
					{
					model.close();
					}
				catch (Exception e)
					{
					e.printStackTrace();
					}
				}
			}
		}
	}
